﻿// -
// <copyright file="SpeechRecognitionClient.cs" company="Microsoft Corporation">
//    Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// -

namespace Hawaii.Services.Speech.Recognition
{
    using System;
    using System.Net;
    using System.Collections.Generic;
    using Hawaii.Services.Client;
    using System.Threading;
    using System.Xml;
    using System.IO;
    using System.Text;
    
    /// <summary>
    /// SpeechRecognitionClient is a helper class for contacting Hawaii Speech Service
    /// for speech recognition. This class accepts a speech buffer as input, sends to the 
    /// speech service and receives a list of recognized texts.
    /// </summary>
    public partial class SpeechRecognitionClient : BaseClientProxy
    {
        private const string ServiceSignature= "SpeechRecognition/{0}";
        private const string SpeechRecognition = "SpeechRecognition";
        private const string DeviceIdName = "DeviceId";
        private const string Result = "Result";

        /// <summary>
        /// A delegate signature for a speech recognition completed event.
        /// </summary>
        /// <param name="sender">Specifies the sender object.</param>
        /// <param name="e">Specifies the event argument.</param>
        public delegate void SpeechRecognitionCompletedEventHandler(object sender, SpeechRecognitionCompletedEventArgs e);

        /// <summary>
        /// [in] Specifies the host name in which the speech service is running.
        /// For example: If the speech service is running in http://stt.hawaii-services.net, the host name
        /// will be 'stt.hawaii-services.net'.
        /// </summary>
        public string HostName { get; set; }

        /// <summary>
        /// [in] Specifies the  mobile client identification for speech recognition.
        /// </summary>
        public string DeviceId { get; set; }

        /// <summary>
        /// [in] Specifies the speech recognition grammar name. 
        /// Currently the only available grammar is 'Dictation' which is a context free speech 
        /// recognition system. More grammars will be made available in the future.
        /// </summary>
        public string Grammar { get; set; }
        
        /// <summary>
        /// [in] Specifies the speech buffer to be recognized. 
        /// The expected speech buffer should be 'SamplesPerSecond=16000', 
        /// 'AudioBitsPerSample=16' and 'AudioChannel=Mono'
        /// </summary>
        public byte[] SpeechBuffer { private get; set; }

        /// <summary>
        /// [out] This is an output parameter which specifies the recognized list of strings.
        /// </summary>
        private List<string> RecognizedTexts { get; set; }

        /// <summary>
        /// Specifies a speech recognition completion event.
        /// </summary>
        public event SpeechRecognitionCompletedEventHandler SpeechRecognitionCompleted;

        /// <summary>
        /// Default constructor
        /// </summary>
        public SpeechRecognitionClient() :
            base(HttpMethod.Post)
        {
            this.HostName = string.Empty;
            this.DeviceId = string.Empty;
            this.SpeechBuffer = null;
            this.RecognizedTexts = null;
            this.Grammar = "Dictation";
        }

        /// <summary>
        /// An overloaded constructor with hostName and deviceId arguments.
        /// </summary>
        /// <param name="hostName">Specifies the host name of the speech service.</param>
        /// <param name="deviceId">Specifies mobile client device id.</param>
        public SpeechRecognitionClient(string hostName, string deviceId) :
            base(HttpMethod.Post)
        {
            this.HostName = hostName;
            this.DeviceId = deviceId;
            this.SpeechBuffer = null;
            this.RecognizedTexts = null;
            this.Grammar = "Dictation";
        }

        /// <summary>
        /// An overloaded constructor with hostName, deviceId and grammarName arguments.
        /// </summary>
        /// <param name="hostName">Specifies the host name of the speech service.</param>
        /// <param name="deviceId">Specifies the mobile client device id.</param>
        /// <param name="grammar">Specifies the grammar loaded in the server.</param>
        public SpeechRecognitionClient(string hostName, string deviceId, string grammar) :
            base(HttpMethod.Post)
        {
            this.HostName = hostName;
            this.DeviceId = deviceId;
            this.SpeechBuffer = null;
            this.RecognizedTexts = null;
            this.Grammar = grammar;
        }

        /// <summary>
        /// An overridden abstract method used to create the speech service uri object.
        /// This method will be called by the base class when it tries to connect to 
        /// the server during a speech recognition service call.
        /// </summary>
        /// <returns></returns>
        protected override Uri ServiceUri()
        {
            // -
            // A service call cannot continue without a valid device id.
            // -
            if(string.IsNullOrEmpty(this.DeviceId))
            {
                throw new Exception("DeviceId is null or empty");
            }
            
            // -
            // Create the service Uri.
            // -
            string signature = string.Format(SpeechRecognitionClient.ServiceSignature, 
                                             this.Grammar);
            ServiceUri uri = new ServiceUri(this.HostName, signature);

            // - 
            // Add query strings, if any. Currently 'DeviceId' is the only 
            // mandatory query string.
            // -
            uri.AddQueryString(SpeechRecognitionClient.DeviceIdName, 
                               Uri.EscapeUriString(this.DeviceId));

            // -
            // Return the URI object.
            // -
            return new Uri(uri.ToString());
        }

        /// <summary>
        /// An overridden abstract method to get the POST data. 
        /// This method will be called by the base class when it needs data 
        /// during a Http POST method call.
        /// </summary>
        /// <returns></returns>
        protected override byte[] GetPostData()
        {
            // -
            // This service client is for speech recognition. 
            // Return the speech buffer. 
            // -
            return this.SpeechBuffer;
        }

        /// <summary>
        /// An overridden abstract method to parse the result from the
        /// speech service.
        /// </summary>
        /// <param name="responseStream"></param>
        protected override void ParseOutput(Stream responseStream)
        {
            // -
            // The following is the output response from the server:
            //
            // <SpeechRecognition result="recognizedString1;recognizeString2;...." />
            //
            // -
            List<string> recognizedItems = null;

            if (responseStream != null)
            {
                using (XmlReader reader = XmlReader.Create(responseStream))
                {
                    while (reader.Read())
                    {
                        if (reader.NodeType == XmlNodeType.Element)
                        {
                            // -
                            // Check for the <SpeechRecognition/> element.
                            // -
                            if (reader.Name.Equals(SpeechRecognitionClient.SpeechRecognition,
                                                   StringComparison.OrdinalIgnoreCase))
                            {
                                while (reader.MoveToNextAttribute())
                                {
                                    // -
                                    // Get the result from the 'result' attribute.
                                    // -
                                    if (reader.Name.Equals(SpeechRecognitionClient.Result,
                                                           StringComparison.OrdinalIgnoreCase))
                                    {
                                        // -
                                        // Parse the result and set the result field 
                                        // of this class.
                                        // -
                                        recognizedItems = ParseRecognized(reader.Value);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (recognizedItems != null)
            {
                this.RecognizedTexts = recognizedItems;
            }
        }

        /// <summary>
        /// Method to parse semi colon (;) delimited texts from the speech service
        /// and create the recognized string list.
        /// </summary>
        /// <param name="recognizedString"></param>
        /// <returns></returns>
        private List<string> ParseRecognized(string recognizedString)
        {
            // -
            // Not a recognized string. This has nothing to do with empty.
            // -
            if (string.IsNullOrEmpty(recognizedString))
            {
                return null;
            }

            // -
            // We have a valid string. Parse it.
            // -
            string[] phrases = recognizedString.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries);
            if (phrases != null && phrases.Length != 0)
            {
                List<string> result = new List<string>();
                result.AddRange(phrases);
                return result;
            }

            return null;
        }

        /// <summary>
        /// Method to invoke the Speech recognition asynchronously.
        /// </summary>
        /// <param name="audioBuffer"></param>
        public void RecognizeSpeechAsync(byte[] audioBuffer)
        {
            // -
            // Set the speech buffer.
            // -
            this.SpeechBuffer = audioBuffer;

            // -
            // Call the speech recognition service in another thread from the
            // thread pool.
            // - 
            ThreadPool.QueueUserWorkItem(RecognizeSpeechThreadFunc, this);
        }

        /// <summary>
        /// Thread function for speech recognition. It is called by a seperate thread 
        /// from the thread pool during an asynchronous service call.
        /// </summary>
        /// <param name="stateInfo"></param>
        private void RecognizeSpeechThreadFunc(object stateInfo)
        {
            SpeechRecognitionClient client = stateInfo as SpeechRecognitionClient;
            // -
            // Call the speech service to recognize speech.
            // -
            bool result = client.ProcessRequest();

            // -
            // Call all client event handling methods.
            // -
            if (client.SpeechRecognitionCompleted != null)
            {
                SpeechRecognitionCompletedEventArgs eventArgs = null;

                // -
                // Create the event argument object based on success/failure.
                // -
                if (result)
                {
                    // - 
                    // A successful recognition.
                    // -
                    eventArgs = new SpeechRecognitionCompletedEventArgs(client.RecognizedTexts);
                }
                else
                {
                    // -
                    // An error in recognition. Set the exception.
                    // -
                    eventArgs = new SpeechRecognitionCompletedEventArgs(this.ErrorException);
                }

                // -
                // Fire the completed event.
                // -
                this.SpeechRecognitionCompleted(client, eventArgs);
            }
        }
    }
}
